package com.gitee.wsl.doc.prosemirror.history.bean

import com.gitee.wsl.doc.prosemirror.history.DEPTH_OVERFLOW
import com.gitee.wsl.doc.prosemirror.history.HistoryOptions
import com.gitee.wsl.doc.prosemirror.history.MAX_EMPTY_ITEMS
import com.gitee.wsl.doc.prosemirror.history.cutOffEvents
import com.gitee.wsl.doc.prosemirror.history.ropesequence.RopeSequence
import com.gitee.wsl.doc.prosemirror.state.EditorState
import com.gitee.wsl.doc.prosemirror.state.bookmark.SelectionBookmark
import com.gitee.wsl.doc.prosemirror.transform.Transform
import com.gitee.wsl.doc.prosemirror.transform.map.Mapping
import com.gitee.wsl.doc.prosemirror.transform.map.StepMap
import kotlin.math.max
import kotlin.math.min

class Branch(val items: RopeSequence<Item>, val eventCount: Int) {

    // Pop the latest event off the branch's history and apply it
    // to a document transform.
    fun popEvent(state: EditorState, preserveItems: Boolean): PopEventResult? {
        if (this.eventCount == 0) return null

        var end = this.items.length
        while (true) {
            val next = this.items.get(end - 1)
            if (next?.selection != null) {
                end--
                break
            }
            end--
        }

        var remap: Mapping? = null
        var mapFrom: Int? = null
        if (preserveItems) {
            remap = this.remapping(end, this.items.length)
            mapFrom = remap.maps.size
        }
        val transform = state.tr
        var selection: SelectionBookmark? = null
        var remaining: Branch? = null
        val addAfter = mutableListOf<Item>()
        val addBefore = mutableListOf<Item>()

        this.items.forEach(this.items.length, 0) { item, i ->
            if (item.step == null) {
                if (remap == null) {
                    remap = this.remapping(end, i + 1)
                    mapFrom = remap.maps.size
                }
                mapFrom = mapFrom!! - 1
                addBefore.add(item)
            } else {
                if (remap != null) {
                    addBefore.add(Item(item.map))
                    val step = item.step.map(remap.slice(mapFrom!!))
                    var map: StepMap? = null

                    if (step != null && transform.maybeStep(step).doc != null) {
                        map = transform.mapping.maps[transform.mapping.maps.size - 1]
                        addAfter.add(Item(map, null, null, addAfter.size + addBefore.size))
                    }
                    mapFrom = mapFrom!! - 1
                    if (map != null) remap.appendMap(map, mapFrom)
                } else {
                    transform.maybeStep(item.step)
                }

                if (item.selection != null) {
                    selection = if (remap != null) item.selection.map(remap.slice(mapFrom!!)) else item.selection
                    remaining = Branch(
                        this.items.slice(0, end).append(addBefore.reversed() + addAfter),
                        this.eventCount - 1
                    )
                    return@forEach false
                }
            }
            true
        }

        return PopEventResult(remaining!!, transform, selection!!)
    }

    // Create a new branch with the given transform added.
    fun addTransform(
        transform: Transform,
        selection: SelectionBookmark?,
        histOptions: HistoryOptions,
        preserveItems: Boolean
    ): Branch {
        var thisSelection = selection
        val newItems = mutableListOf<Item>()
        var eventCount = this.eventCount
        var oldItems = this.items
        var lastItem = if (!preserveItems && oldItems.length != 0) {
            oldItems.get(oldItems.length - 1)
        } else {
            null
        }

        for (i in 0 until transform.steps.size) {
            val step = transform.steps[i].invert(transform.docs[i])
            var item = Item(transform.mapping.maps[i], step, thisSelection)
            val merged = lastItem?.merge(item)
            if (merged != null) {
                item = merged
                if (i != 0) {
                    newItems.removeLast()
                } else {
                    oldItems = oldItems.slice(0, oldItems.length - 1)
                }
            }
            newItems.add(item)
            if (thisSelection != null) {
                eventCount++
                thisSelection = null
            }
            if (!preserveItems) {
                lastItem = item
            }
        }
        val overflow = eventCount - histOptions.depth
        if (overflow > DEPTH_OVERFLOW) {
            oldItems = oldItems.cutOffEvents( overflow)
            eventCount -= overflow
        }
        return Branch(oldItems.append(newItems), eventCount)
    }

    fun remapping(from: Int, to: Int): Mapping {
        val maps = Mapping()
        this.items.forEach(from, to) { item, i ->
            val mirrorPos = if (item.mirrorOffset != null && i - item.mirrorOffset >= from) {
                maps.maps.size - item.mirrorOffset
            } else {
                null
            }
            maps.appendMap(item.map, mirrorPos)
            true
        }
        return maps
    }

    fun addMaps(array: List<StepMap>): Branch {
        if (this.eventCount == 0) return this
        return Branch(this.items.append(array.map { map -> Item(map) }), this.eventCount)
    }

    /** When the collab module receives remote changes, the history has
     * to know about those, so that it can adjust the steps that were
     * rebased on top of the remote changes, and include the position
     * maps for the remote changes in its array of items.
     */
    fun rebased(rebasedTransform: Transform, rebasedCount: Int): Branch {
        if (this.eventCount == 0) return this

        val rebasedItems = mutableListOf<Item>()
        val start = max(0, this.items.length - rebasedCount)

        val mapping = rebasedTransform.mapping
        var newUntil = rebasedTransform.steps.size
        var eventCount = this.eventCount
        this.items.forEach(
            start
        ) { item, _ ->
            if (item.selection != null) eventCount--
            true
        }

        var iRebased = rebasedCount
        this.items.forEach(start) { item, _ ->
            val pos = mapping.getMirror(--iRebased) ?: return@forEach true
            newUntil = min(newUntil, pos)
            val map = mapping.maps[pos]
            if (item.step != null) {
                val step = rebasedTransform.steps[pos].invert(rebasedTransform.docs[pos])
                val selection = item.selection?.map(mapping.slice(iRebased + 1, pos))
                if (selection != null) eventCount++
                rebasedItems.add(Item(map, step, selection))
            } else {
                rebasedItems.add(Item(map))
            }
        }

        val newMaps = mutableListOf<Item>()
        for (i in rebasedCount until newUntil) {
            newMaps.add(Item(mapping.maps[i]))
        }
        val items = this.items.slice(0, start).append(newMaps).append(rebasedItems)
        var branch = Branch(items, eventCount)

        if (branch.emptyItemCount() > MAX_EMPTY_ITEMS) {
            branch = branch.compress(this.items.length - rebasedItems.size)
        }
        return branch
    }

    fun emptyItemCount(): Int {
        var count = 0
        this.items.forEach { item, _ ->
            if (item.step == null) {
                count++
            }
            true
        }
        return count
    }

    /** Compressing a branch means rewriting it to push the air (map-only
     * items) out. During collaboration, these naturally accumulate
     * because each remote change adds one. The `upto` argument is used
     * to ensure that only the items below a given level are compressed,
     * because `rebased` relies on a clean, untouched set of items in
     * order to associate old items with rebased steps
     */
    fun compress(upto: Int = this.items.length): Branch {
        val remap = this.remapping(0, upto)
        var mapFrom = remap.maps.size
        val items = mutableListOf<Item>()
        var events = 0
        this.items.forEach(this.items.length, 0) { item, i ->
            if (i >= upto) {
                items.add(item)
                if (item.selection != null) events++
            } else if (item.step != null) {
                val step = item.step.map(remap.slice(mapFrom))
                val map = step?.getMap()
                mapFrom--
                if (map != null) remap.appendMap(map, mapFrom)
                if (step != null) {
                    val selection = item.selection?.map(remap.slice(mapFrom))
                    if (selection != null) events++
                    val newItem = Item(map!!.invert(), step, selection)
                    val merged = items.lastOrNull()?.merge(newItem)
                    val last = items.size - 1
                    if (merged != null) {
                        items[last] = merged
                    } else {
                        items.add(newItem)
                    }
                }
            } else {
                mapFrom--
            }
            true
        }
        return Branch(RopeSequence.Companion.from(items.reversed()), events)
    }

    override fun toString(): String {
        return "Branch(items: ${items::class.simpleName} eventCount:$eventCount)"
    }

    companion object {
        val empty = Branch(RopeSequence.Companion.empty(), 0)
    }
}